/// <reference path="../m3u8-parser.d.ts" />

import path from 'node:path'
import { existsSync } from 'node:fs'
import { exec } from 'node:child_process'
import type { CipherCCMTypes } from 'node:crypto'
import crypto, { createDecipheriv } from 'node:crypto'
import { readFile, writeFile } from 'node:fs/promises'
import { assignIn } from 'lodash-es'
import type { Segment } from 'm3u8-parser'
import { Parser } from 'm3u8-parser'
import { PromisePool } from '@supercharge/promise-pool'
import download from 'download'
import { ensureDirSync, ensureFileSync } from 'fs-extra'

export interface Alias {
  key_key: string
  index_m3u8: string
  index_m3u8_back: string
  mp4: ((key: string) => string) | string
}

export interface Options {
  key: string
  url: string
  target: string
  alias: Alias
  concurrency: number
}

export type OptionsWithRequiredFields = Required<Pick<Options, 'key' | 'url'>> & Partial<Options>

export async function createDownload(_options: OptionsWithRequiredFields) {
  const options: Options = {
    key: '',
    url: '',
    target: path.join(__dirname),
    alias: {
      key_key: 'key.key',
      index_m3u8: 'index.m3u8',
      index_m3u8_back: 'index.m3u8.back',
      mp4: (key: string) => `${key}.mp4`,
    },
    concurrency: 5,
  }

  assignIn(options, _options)

  const output_m3u8 = path.join(options.target, options.alias.index_m3u8)
  const output_key = path.join(options.target, options.alias.key_key)
  const output_mp4 = typeof options.alias.mp4 === 'string'
    ? path.join(options.target, options.alias.mp4)
    : path.join(options.target, options.alias.mp4(options.key))

  const segments = await parseM3u8(options.url)

  let aes = ''
  const { errors, results } = await PromisePool
    .for(segments)
    .useCorrespondingResults()
    .withConcurrency(options.concurrency)
    .onTaskStarted(() => {

    })
    .onTaskFinished(() => {

    })
    .handleError((_error, _item, pool) => {
      pool.stop()
    })
    .process(async (segment, index) => {
      aes = aes || await parseKey(segment)

      const output_ts_name = `${`${index}`.padStart(4, '0')}.ts`
      const output_ts_temp = path.join(options.target, 'temp')
      const output_ts = path.join(options.target, 'temp', output_ts_name)
      ensureDirSync(output_ts_temp)

      if (existsSync(output_ts))
        return output_ts

      const stream = await download(segment.uri)
      const iv = crypto.randomBytes(16)
      const algorithm: CipherCCMTypes = `${segment.key.method}-cbc`.toLowerCase() as CipherCCMTypes
      const cipher = createDecipheriv(algorithm, aes, iv)
      cipher.on('error', console.error)

      const segmentData = Buffer.concat([cipher.update(stream), cipher.final()])

      await writeFile(output_ts, segmentData)

      return output_ts
    })

  if (errors.length) {
    console.error(`存在异常${errors.length}`, errors[0])
  }
  else {
    console.log('results', results)
    await mergeTs(results as string[])
  }

  async function parseM3u8(url: string) {
    const content = existsSync(output_m3u8)
      ? await readFile(output_m3u8, {
        encoding: 'utf-8',
      })
      : (await download(url, options.target, {
          filename: options.alias.index_m3u8_back,
        })).toString()

    const parser = new Parser()
    parser.push(content)
    parser.end()

    return parser.manifest.segments
  }

  async function parseKey(segment: Segment) {
    if (!existsSync(output_key))
      await download(segment.key!.uri, options.target)

    return await readFile(output_key, { encoding: 'utf-8' })
  }

  async function mergeTs(results: string[]) {
    const tempPath = path.join(options.target, 'temp.txt')
    ensureFileSync(tempPath)
    const tempContent = results.filter(Boolean).map(url => `file '${url as string}'`).join('\r')

    await writeFile(
      tempPath,
      tempContent,
      'utf-8',
    )
    await ffmpegMerge(tempPath)
  }

  function ffmpegMerge(input: string): Promise<string> {
    const cmd = `ffmpeg -f concat -safe 0 -i ${input} -c copy -y ${output_mp4}`
    return new Promise((resolve) => {
      exec(cmd, (err) => {
        err && console.error(`exec error: ${err}`)

        resolve(output_mp4)
      })
    })
  }
}
